package edu.alaska.uaa.cs401.fgm;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;

public class FanoronaServer extends Thread {
	//FanoronaBoardInterface board = new FanoronaLinkBoard();
	FanoronaBoardInterface board = null;

	private final FanoronaBoardDisplayerInterface DEFAULT_DISPLAYER = new FanoronaBoardDisplayerInterface() {
		public void update() {
			
		}
		public String requestNewLogFile() {
			return null;
			
		}
	};
	FanoronaBoardDisplayerInterface displayer = DEFAULT_DISPLAYER; 
	private short port = 3266;
	private ServerSocket sock;
	private String blackname = "";
	private String whitename = "";
	private ArrayList<String> status = new ArrayList<String>();
	private Map<StatusPriority,Set<String> > msgpriorities = new TreeMap<StatusPriority,Set<String> >();
	private UUID gameUUID = null;
	private int totalGames = -1;
	
	private final short DEFAULT_PORT = 3266; // IANA registered NS CFG Server
											 // -OR- this might be FANO on a telephone.
											 // IANA ain't got nothin' on telephones!
	private String loggingDirectory;
	

	public enum StatusPriority {
		DEBUG,
		HIGH;
	}
	
	{
		for (StatusPriority p : StatusPriority.values()) {
			msgpriorities.put(p, new HashSet<String>());
		}
		status.add("Initializing");
	}
	
	public void setStatus(String status,StatusPriority p) {
		msgpriorities.get(p).add(status);
		this.status.add(status);
		displayer.update();
	}
	
	public String getStatus(StatusPriority min) {
		String ret;
		ret = "No status available";
		for (int i = status.size()-1; i >= 0; i--) {
			ret = status.get(i); //if there's nothing else, we'll end up returning
								 // "Initializing"
			for (StatusPriority p : StatusPriority.values()) {
				if (p.ordinal() >= min.ordinal()) {
					if (msgpriorities.get(p).contains(ret)) return ret;
				}
			}
		}
		return ret;
	}
	
	public Iterable<String> getStatusArray(StatusPriority min) {
		ArrayList<String> ret = new ArrayList<String>(status.size());
		for (String msg : status) {
			for (StatusPriority p : StatusPriority.values()) {
				if (p.ordinal() >= min.ordinal()) {
					// this is above the min.
					if (msgpriorities.get(p).contains(msg)) {
						// and contains the message in question.  add it to ret
						ret.add(msg);
					}
				}
			}
		}
		return ret;
	}
	
	public FanoronaServer() throws IOException {
//		port = -1;
//		sock = new ServerSocket();
//		try {
//			sock.setReuseAddress(true); // no TIME_WAIT kthx.
//		} catch (SocketException e) {
//			System.err.format("Failed to set SO_REUSEADDR.  This is non-fatal but beware\nthat the server may not be restarted on the same port for a short time.\n");
//		} 
	}
	
	public synchronized FanoronaServer setPort(short port) {
		while (true) {
			this.port = port;
			try {
				sock.bind(new InetSocketAddress(this.port)); // wildcard address
				break;
			} catch (IOException e) {
				// do nothing.  bind failed.  try another port!
			}
			this.port++;
		}
		return this;
	}
	
	public FanoronaServer setShutdownAfterNGames(int x) {
		// if this isn't called, it will default to never shut down.
		totalGames = x;
		return this;
	}
	
	public void run() {
		port=3266;
		try {
			sock = new ServerSocket(3266);
			//sock.bind(new InetSocketAddress(3266));
		} catch (IOException e2) {
			// TODO Auto-generated catch block
			setStatus("Failed to bind to port",StatusPriority.HIGH);
			return;
		}
		try {
			sock.setReuseAddress(true); // no TIME_WAIT kthx.
		} catch (SocketException e) {
			setStatus("Failed to set SO_REUSEADDR.  This is non-fatal but beware that the server may not be restarted on the same port for a short time.",StatusPriority.HIGH);
		} 
		int iter = 0;
		while (totalGames-- != 0) {
			board = new FanoronaLinkBoard();
			if (totalGames < 0) totalGames = -1; // run forever.
			System.out.format("Server running iteration #%d\n",++iter);
			Socket socka=null;
			BufferedWriter aw = null;
			BufferedReader ar = null;
			Socket sockb=null;
			BufferedWriter bw = null;
			BufferedReader br = null;
			
			BufferedReader blackr = null;
			BufferedReader whiter = null;
			BufferedWriter blackw = null;
			BufferedWriter whitew = null;
			try {
				setStatus("Waiting for connection 1",StatusPriority.HIGH);
				socka = sock.accept();
				aw = new BufferedWriter(new OutputStreamWriter(socka.getOutputStream()));
				ar = new BufferedReader(new InputStreamReader(socka.getInputStream()));
				setStatus("Waiting for connection 2",StatusPriority.HIGH);
				sockb = sock.accept();
				bw = new BufferedWriter(new OutputStreamWriter(sockb.getOutputStream()));
				br = new BufferedReader(new InputStreamReader(sockb.getInputStream()));
				setStatus("Waiting for clients to initialize",StatusPriority.HIGH);

				if (Math.random() < .5) {
					blackr = ar;
					blackw = aw;
					whiter = br;
					whitew = bw;
				} else {
					blackr = br;
					blackw = bw;
					whiter = ar;
					whitew = aw;
				}
				// we have two sockets!  let's start the game!!!!!!! ZOMG
				
				gameUUID = java.util.UUID.randomUUID();
				whitename = whiter.readLine();
				whitew.write("+white "+gameUUID.toString()+"\n");
				whitew.flush();
				blackname = blackr.readLine();
				blackw.write("+black "+gameUUID.toString()+"\n");
				blackw.flush();

				setStatus(String.format("W:%s vs B:%s",whitename,blackname),StatusPriority.HIGH);

				displayer.update();
				
				FanoronaColor currentMover = FanoronaColor.White;
				
				while (board.getWinner() == null) {
					setStatus("Getting moves for "+currentMover,StatusPriority.DEBUG);
					BufferedReader cr = null;
					BufferedWriter cw = null;
//					BufferedReader or = null;
					BufferedWriter ow = null;
					switch (currentMover) {
					case White:
						cr = whiter;
						cw = whitew;
//						or = blackr;
						ow = blackw;
						break;
					case Black:
						cr = blackr;
						cw = blackw;
//						or = whiter;
						ow = whitew;
						break;
					}

					String l = null;
					while (true) {
						try {
							l=cr.readLine();
							if (l == null) throw new IOException();
							board.processMove(l,currentMover);
							setStatus(String.format("Moves for %s succeeded!\n",currentMover),StatusPriority.DEBUG);
							break;
						} catch (FanoronaMoveException e) {
							setStatus(String.format("received an invalid move from %s: %s",currentMover,e.getErrorText()),StatusPriority.DEBUG);
							cw.write("-"+e.getErrorText()+"\n");
							cw.flush();
						}
					}
					if (board.getWinner() != null) {
						ow.write("-");
						cw.write("-\n");
						cw.flush();
					} else {
						ow.write("+");
						cw.write("+\n");
						cw.flush();
					}
					ow.write(l+"\n");
					ow.flush();
					System.out.format("%s: %s\n",currentMover,l);
					currentMover = currentMover.opposite();
					displayer.update();
				}				
			} catch (Exception e) {
				try {
					whitew.write("-An unrecoverable error occured.\n");
				} catch (IOException e1) {
				}
				try {
					blackw.write("-An unrecoverable error occured.\n");
				} catch (IOException e1) {
				}
				System.err.format("An unrecoverable error occured.\n");
				setStatus("An unrecoverable error occured during the game.",StatusPriority.HIGH);
				e.printStackTrace();
				try {
					socka.close();
				} catch (IOException e1) {
				}
				try {
					sockb.close();
				} catch (IOException e1) {
				}
			}
			setStatus(String.format("GameID: %s W:%s vs B:%s; %s won",gameUUID,whitename,blackname,board.getWinner()),StatusPriority.HIGH);
			if (this.loggingDirectory != null) {
				String outfile = String.format("%s%s%s.log", this.loggingDirectory,File.separator,gameUUID.toString());
				while (true) {
					if (outfile == null) {
						setStatus(String.format("No new filename provided... ignoring."),StatusPriority.HIGH);
						break;
					}
					try {
						BufferedWriter fw = new BufferedWriter(new FileWriter(outfile));
						fw.write(String.format("GameID: %s W:%s vs B:%s; %s won",gameUUID,whitename,blackname,board.getWinner()));
						fw.write(board.getLog());
						fw.flush();
						fw.close();
						break;
					} catch (IOException e) {
						setStatus(String.format("Failed to save log in %s"),StatusPriority.HIGH);
						outfile = displayer.requestNewLogFile();
					}
				}
			}
			displayer.update();
		}
	}
	
	public void setLoggingDirectory(String d) {
		d = new File(d).getAbsolutePath();
		setStatus("Logging directory set to '"+d+"'", StatusPriority.HIGH);
		this.loggingDirectory = d;
	}
	
	public FanoronaBoardInterface getBoard() {
		return board;
	}

	public void setDisplayer(FanoronaServerFrame fanoronaServerFrame) {
		// on update, this should be used to call out an update.
		// TEST: try it and see if it updates.
		if (fanoronaServerFrame==null) {
			displayer=DEFAULT_DISPLAYER;
		} else {
			this.displayer = fanoronaServerFrame;
		}
		displayer.update();
	}

	public short getPort() {
		if (this.port == -1) {
			setPort(DEFAULT_PORT);
		}
		return port;
	}
}
